package com.authorization.config;

import com.authorization.controller.MySessionInformationExpiredStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

/**
 * @author chengyadong
 * @date 2023/12/8 13:43
 * @description 授权服务器认证登录配置
 */
@Configuration
@EnableWebSecurity(debug = true)
public class DefaultSecurityConfig {
	//自定义身份验证异常实现类
	@Bean
	public AuthenticationEntryPointCustomer authenticationEntryPointCustomer() {
		return new AuthenticationEntryPointCustomer();
	}

	@Autowired
	MySessionInformationExpiredStrategy mySessionInformationExpiredStrategy;

	//用于认证的Spring Security过滤器链
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests((authorizeHttpRequests) ->
				//当请求路径匹配到 /js/** , /css/** 这类静态资源的时候，不做认证，直接放行,其他请求进行拦截
				authorizeHttpRequests.requestMatchers("/js/**", "/css/**").permitAll()
						.anyRequest().authenticated()
		);
		//注意loginPage，failureUrl，defaultSuccessUrl都是指向的jsp页面，对于前后端分离项目不需要进行配置
		http.formLogin((formLogin) -> formLogin
						//登录成功后的处理，我们可以把登录信息进行缓存或者记录登录日志等操作
						.successHandler(new SucessHandlerCustomer())
						.failureHandler(new FailureHandlerCustomer())
						//配置登录用户名参数名（前端传递的用户名对应的字段，默认为username）
						.usernameParameter("userName")
						//配置登录密码参数名（前端传递的密码对应的字段，默认为password）
						.passwordParameter("passWord")
						//配置登录界面的路径(自定义)
						.loginPage("/login")
						//配置登录失败后访问的页面路径(自定义)
						//.failureUrl("/login.html?error")
						//loginProcessingUrl()配置登录按钮的路径。默认POST方法的/login,如果有自定义的登录页面，我们响应登录按钮的请求可能是/a或者/b等，此处需要指定进行放行
						//此处不需要增加permitAll(),过滤器内部帮我们进行了处理 AbstractAuthenticationProcessingFilter会对该路径拦截处理
						.loginProcessingUrl("/doLogin")
				//登录认证成功后默认转跳的路径，第二个参数为true则任何情况都跳到指定url。否则会先跳到referer，referer为空才跳到指定url，比如我们的首页菜单页
				/*.defaultSuccessUrl("/portal/index", true)*/
		);

		//20231209暂时使用系统自带的登录调整
		/*http.formLogin(Customizer.withDefaults());*/

		http.logout((logout) -> logout.logoutUrl("/logout")
				/*.logoutSuccessHandler()*/
				//登出后让session直接过期
				.invalidateHttpSession(true)
				//清除认证信息
				.clearAuthentication(true));
		//增加异常捕获(比如未登录时默认会重定向其自带的登录页面，我们通过该捕获可以直接向前端返回异常避免重定向)--仅限登录执行链
		http.exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(authenticationEntryPointCustomer()));
		//关闭csrf，前后端分离架构不需要csrf保护
		http.csrf((csrf) -> csrf.disable());
		//前后端分离时关闭跨域--20240304
		http.cors((cors)->cors.disable());
		http.sessionManagement((sm)->sm.maximumSessions(1)     //每个用户在系统中的最大session数
				.expiredSessionStrategy(mySessionInformationExpiredStrategy)  // 当用户达到最大session数后，则调用此处的实现
				.maxSessionsPreventsLogin(false)          // 允许后续登录覆盖前一个登录
				.sessionRegistry(sessionRegistry()));
		return http.build();
	}
	// 没限制住session个数是因为 ConcurrentMap属性对应的key是一个对象，每次登录的时候都是一个不同的对象，但是里面的userName确实一样，所以应该按照名称的粒度作为
	// key值。  而且如果是多台服务器，我们需要将session存储到共享设备中--Redis
	@Bean
	public SessionRegistry sessionRegistry() {
		return new SessionRegistryImpl();
	}

	//密码的加密比对方式有多种，都是实现了PasswordEncoder接口，springSecurity默认情况下帮我们使用的是DelegatingPasswordEncoder，其是一种代理，要求我们再将密码加密
	//完成存入数据库时增加前缀(该前缀标志着当前密码使用的哪种加密器) ，再比对的时候DelegatingPasswordEncoder会根据前缀帮我们选择相应的加密器进行比对，这种方式的好处是数据库
	//中的密码可以使用多种样式的加密器 缺点：密码加密后需要我们拼接标志
	// 如果我们想要使用某一种固定的加密器，那么可以通过注入的方式
	/*@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}*/

	/**
	 * 开放一些端点的访问控制 不需要认证就可以访问的端口  每一个路径均会形成一个DefaultSecurityFilterChain,但是里面的List<Filter> filters属性为null
	 *
	 * @return
	 */
	@Bean
	public WebSecurityCustomizer ignoringCustomizer() {
		return (web) -> web.ignoring().requestMatchers("/ignore1", "/ignore2");
	}

	/**
	 * 模拟用户
	 *
	 * @return
	 */
/*	@Bean
	UserDetailsService users() {
		UserDetails user = User.builder()
				.username("admin")
				.password("1234567")
				.passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
				.roles("admin")
				.authorities("test2")
				.build();
		return new InMemoryUserDetailsManager(user);
	}*/

}
